package cn.springcloud.fix.oauth2.autoconfigure.client;

import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
import org.springframework.util.StringUtils;

@Configuration
@ConditionalOnClass(EnableOAuth2Client.class)
public class OAuth2RestOperationsConfiguration {

    @Configuration
    @Conditional(ClientCredentialsCondition.class)
    protected static class SingletonScopedConfiguration {

        @Bean
        @ConfigurationProperties(prefix = "security.oauth2.client")
        @Primary
        public ClientCredentialsResourceDetails oauth2RemoteResource() {
            return new ClientCredentialsResourceDetails();
        }

        @Bean
        public DefaultOAuth2ClientContext oauth2ClientContext() {
            return new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
        }
    }

    @Configuration
    @ConditionalOnBean(OAuth2ClientConfiguration.class)
    @Conditional({OAuth2ClientIdCondition.class, NoClientCredentialsCondition.class})
    @Import(OAuth2ProtectedResourceDetailsConfiguration.class)
    protected static class SessionScopedConfiguration {

        @Bean
        public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter, SecurityProperties security) {
            FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<>();
            registration.setFilter(filter);
            registration.setOrder(security.getFilter().getOrder() - 10);
            return registration;
        }
    }

    // When the authentication is per cookie but the stored token is an oauth2 one,
    // we can
    // pass that on to a client that wants to call downstream. We don't even need an
    // OAuth2ClientContextFilter until we need to refresh the access token. To
    // handle
    // refresh tokens you need to @EnableOAuth2Client
    @Configuration
    @ConditionalOnMissingBean(OAuth2ClientConfiguration.class)
    @Conditional({OAuth2ClientIdCondition.class, NoClientCredentialsCondition.class})
    @Import(OAuth2ProtectedResourceDetailsConfiguration.class)
    protected static class RequestScopedConfiguration {

        @Bean
        @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
        public DefaultOAuth2ClientContext oauth2ClientContext() {
            DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest());
            Authentication principal = SecurityContextHolder.getContext().getAuthentication();
            if (principal instanceof OAuth2Authentication) {
                OAuth2Authentication authentication = (OAuth2Authentication) principal;
                Object details = authentication.getDetails();
                if (details instanceof OAuth2AuthenticationDetails) {
                    OAuth2AuthenticationDetails oauthsDetails = (OAuth2AuthenticationDetails) details;
                    String token = oauthsDetails.getTokenValue();
                    context.setAccessToken(new DefaultOAuth2AccessToken(token));
                }
            }
            return context;
        }
    }

    /**
     * Condition to check if a security.oauth2.client.client-id is specified(具体说明).
     */
    static class OAuth2ClientIdCondition extends SpringBootCondition {

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String clientId = context.getEnvironment().getProperty("security.oauth2.client.client-id");
            ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth Client ID");
            if (StringUtils.hasLength(clientId)) {
                return ConditionOutcome.match(message.foundExactly("security.oauth2.client.client-id property"));
            }
            return ConditionOutcome.noMatch(message.didNotFind("security.oauth2.client.client-id property").atAll());
        }
    }

    /**
     * Condition to check for no client credentials.
     */
    static class NoClientCredentialsCondition extends NoneNestedConditions {

        NoClientCredentialsCondition() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @Conditional(ClientCredentialsCondition.class)
        static class ClientCredentialsActivated {

        }
    }

    static class ClientCredentialsCondition extends AnyNestedCondition {

        ClientCredentialsCondition() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @ConditionalOnProperty(prefix = "security.oauth2.client", name = "grant-type", havingValue = "client_credentials")
        static class ClientCredentialsConfigured {

        }

        @ConditionalOnNotWebApplication
        static class NoWebApplication {

        }
    }
}
